You Don't Know JS上卷Part2 Chapter3.对象

3.1 语法

文字形式和构造形式生成的对象是一样的。

  • 文字(声明)形式

    1
    2
    3
    4
    var myObj = {
    key: value
    // ...
    }
  • 构造形式

    1
    2
    var myObj = new Object();
    myObj.key = value;

3.2 类型

  • 基本数据类型:stringnumberbooleannullundefinedsymbol

    • 上述6种数据类型本身不是对象,”JS中万物皆是对象“的说法是错误的
    • typeof null返回字符串”object“,实际上,null本身是基本数据类型,这是语言本身的一个bug。(原理是这样:不同的对象在底层都表示为二进制,在JS中二进制前三位都为0的话会被判断为object类型,null的二进制表示是全0,自然前三位也是0,所以执行typeof时会返回”object“)
  • 引用数据类型:统称为object类型,称作对象子类型(内置对象),细分有StringNumberBooleanObjectFunctionArrayDateRegExpError
1
2
3
4
5
6
7
8
9
10
11
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false

var strObject = new String("I am a string");
typeof strObject; // "object"
strObject instanceof String; // true

// 检查sub-type对象
// 可以认为子类型在内部借用了Object中的toString()方法
Object.prototype.toString.call( strObject ); // [object String]

上述strPrimitive并不是一个对象,只是一个字面量,并且是一个不可变的值。如果需要执行一些操作,比如获取长度、访问其中某个字符等,那需要转换成String对象。

  • JS引擎会自动把字面量转换成String对象,所以可以访问属性和方法。数值字面量和布尔字面量同理。
1
2
3
4
5
var strPrimitive = "I am a string";

console.log( strPrimitive.length ); // 13

console.log( strPrimitive.charAt( 3 ) ); // "m"
  • null和undefined没有对应的构造形式,Date只有构造,没有文字形式。
  • Object、Array、Function和RegExp(正则表达式),无论使用文字形式还是构造形式,它们都是对象,不是字面量。

3.3 内容

  • 对象的内容是由一些存储在特定命名位置的值(任意类型的)组成的,称之为属性
  • 引擎内部,属性的值一般不保存在对象容器内部,存储在对象容器内部的是这些属性的名称,通过引用指向这些值真正的存储位置。
  • .a语法称为”属性访问“,[“a”]语法称为”键访问“。它们访问的是同一个位置。
1
2
3
4
5
6
var myObject = {
a: 2
};

myObject.a; // 2 属性 <----> 值
myObject["a"]; // 2 键 <----> 值
  • [“..”]语法使用字符串来访问属性,可以在程序中构造这个字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
var myObject = {
a: 2
};

var idx;

if (true) {
idx = "a";
}

// 之后

console.log( myObject[idx] ); // 2
  • 对象属性名永远是字符串,如果是其他类型,则会转换为一个字符串,数字也不例外。
1
2
3
4
5
6
7
8
9
var myObject = { };

myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";

myObject["true"]; // "foo";
myObject["3"]; // "bar";
myObject["[object Object]"]; // "baz";
3.3.1 可计算属性名

ES6增加可计算属性名,可以在文字形式中使用[ ]包裹一个表达式来当做属性名

1
2
3
4
5
6
7
8
9
var prefix = "foo";

var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world"
};

myObject["foobar"]; // hello
myObject["foobaz"]; // world
3.3.2 属性与方法

技术角度来说,函数永远不会”属于“一个对象。有些函数具有this引用,有时候这些this确实会指向调用位置的对象引用。但是这是this在运行时根据调用位置动态绑定的,本质上并没有把一个函数变成一个”方法“,函数和对象的关系最多只能是间接关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
console.log( "foo" );
}

var someFoo = foo; // 对foo的变量引用

var myObject = {
someFoo: foo
};

// 对同一个对象的不同引用
foo; // function foo(){..}

someFoo; // function foo(){..}

myObject.someFoo; // function foo(){..}
3.3.3 数组
  • 数组期望的是数值下标,索引是非负数。
1
2
3
4
5
6
7
var myArray = [ "foo", 42, "bar" ];

myArray.length; // 3

myArray[0]; // "foo"

myArray[2]; // "bar"
  • 数组也是对象,可以给数组添加属性。(数组的length值并未发生变化)
1
2
3
4
5
6
7
var myArray = [ "foo", 42, "bar" ];

myArray.baz = "baz";

myArray.length; // 3

myArray.baz; // "baz"
  • 如果添加的属性名”看起来“像一个数字,那它会变成一个数值下标。会修改数组的内容而不是添加一个属性。
1
2
3
4
5
6
7
var myArray = [ "foo", 42, "bar" ];

myArray["3"] = "baz";

myArray.length; // 4

myArray[3]; // "baz"
3.3.4 复制对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function anotherFunction() { /*..*/ }

var anotherObject = {
c: true
};

var anotherArray = [];

var myObject = {
a: 2,
b: anotherObject, // 引用,不是复本!
c: anotherArray, // 另一个引用!
d: anotherFunction
}

anotherArray.push( anotherObject, myObject );
  • 对于浅拷贝(浅复制):新对象中的a是复制的,但是b、c、d只是三个引用,和旧对象中是一样的。

    • Object.assign(..),ES6新增,第一个参数是目标对象,之后跟一个或多个源对象。遍历所有源对象的所有可枚举(enumerable)的自有键(owned key),并把它们复制(使用=操作符赋值)到目标对象,最后返回目标对象。

      1
      2
      3
      4
      5
      6
      var newObj = Object.assign( {}, myObject );

      newObj.a; // 2
      newObj.b === anotherObject; // true
      newObj.c === anotherArray; // true
      newObj.d === anotherFunction; // true
  • 对于深拷贝(深复制):除了复制myObject以外,还会复制anotherObject和anotherArray,问题在于anotherArray还引用了anotherObject和myObject,这样就由于循环引用导致死循环。

    • JSON安全(可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)

      1
      var newObj = JSON.parse( JSON.stringify( someObj ) );
3.3.5 属性描述符

ES5开始,所有的属性都具备了属性描述符(数据描述符)。

  • Object.getOwnPropertyDescriptor(..):获取对象属性对应的属性描述符
1
2
3
4
5
6
7
8
9
10
11
var myObject = {
a: 2
};

Object.getOwnPropertyDescriptor( myObject, "a");
// {
// value: 2,
// writable: true, // 可写
// enumerable: true, // 可枚举
// configurable: true // 可配置
// }
  • Object.defineProperty(..):添加新属性或者修改已有属性(configurable为true)
1
2
3
4
5
6
7
8
9
10
var myObject = {};

Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
enumerable: true,
configurable: true
});

myObject.a; // 2
  • 1、Writable:属性的值是否可修改。

    • false时,对于属性值的修改静默失败(silently failed)。严格模式下,产生TypeError错误。
  • 2、configurable:属性是否可配置。

    • true时,使用defineProperty(..)修改属性描述符。
    • false时,尝试使用defineProperty(..)会产生TypeError错误(无论是不是严格模式)。改成false是单向操作,无法撤销!
    • false时,还是可以把writable的状态由true改为false,但是无法由false改为true。
    • false时,禁止删除这个属性。delete myObject.a会静默失败。
  • 3、enumerable:属性是否会出现在对象的属性枚举中,比如说for..in循环
3.3.6 不变性

实现属性或者对象不可改变的方法有如下多种,但是所有的方法创建的都是浅不变性,只会影响目标对象和它的直接属性。

  • 1、对象常量

    writable:falseconfigurable:false可以创建常量属性(不可修改、重定义或者删除)。

    1
    2
    3
    4
    5
    6
    7
    var myObject = {};

    Object.defineProperty( myObject, "FAVORITE_NUMBER", {
    value: 42,
    writable: false,
    configurable: false
    } );
  • 2、禁止扩展

    Object.preventExtensions(..),禁止一个对象添加新属性并且保留已有属性。

    非严格模式下,创建属性b会静默失败。严格模式下,抛出TypeError错误。

    1
    2
    3
    4
    5
    6
    7
    8
    var myObject = {
    a:2
    };

    Object.preventExtensions( myObject );

    myObject.b = 3;
    myObject.b; // undefined
  • 3、密封

    Object.seal(..),密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(但是可以修改属性的值)。

    内部会调用Object.preventExtensions(..)并把所有现有属性标记为configurable:false

  • 4、冻结

    Object.freeze(..),禁止对于对象本身及其任意直接属性的修改(引用对象不受影响)。

    内部调用Object.seal(..)并把所有现有属性标记为writable:false。这样就无法修改属性的值。

  • 5、深度冻结

    在这个对象上调用Object.freeze(..),然后遍历它引用的所有对象并在这些对象上调用Object.freeze(..)。但是这样有可能会在无意中冻结其他(共享)对象。

3.3.7 [[Get]]
1
2
3
4
5
var myObject = {
a: 2
};

myObject.a; // 2
  • 对象默认的[[Get]]操作可以控制属性值的获取

  • myObject.a在myObject上实现了[[Get]]操作(有点像函数调用:[[Get]]())。

1
2
3
4
5
6
7
8
9
10
11
12
13
st=>start: Start
e=>end: End
op1=>operation: myObject.a执行[[Get]]操作
cond1=>condition: 对象中是否有名称相同的属性
op2=>operation: 查找成功,返回属性值
op3=>operation: 没有找到,遍历可能存在的[[Prototype]]链
cond2=>condition: 原型链是否查找成功
op4=>operation: 最终没有找到,返回undefined
st->op1->cond1
cond1(yes)->op2->e
cond1(no)->op3->cond2
cond2(yes)->op2
cond2(no)->op4->e

变量访问和对象属性访问的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
st=>start: Start
e=>end: End
cond1=>condition: 变量访问
op1=>operation: 查找词法作用域
op11=>operation: 对象属性访问
op2=>operation: [[Get]]操作
cond3=>condition: 是否查找成功
op3=>operation: 查找成功,返回变量值
op4=>operation: 查找失败,抛出
ReferenceError异常
cond4=>condition: 是否查找成功
op5=>operation: 查找成功,返回属性值
op6=>operation: 查找失败,
返回undefined

st->cond1
cond1(yes)->op1->cond3
cond1(no)->op11->op2->cond4
cond3(yes)->op3->e
cond3(no)->op4->e
cond4(yes)->op5->e
cond4(no)->op6->e
  • 由于仅根据返回值无法判断出到底变量的值为undefined还是变量不存在,所以[[Get]]操作返回了undefined
1
2
3
4
5
6
7
var myObject = {
a: undefined
};

myObject.a; // undefined

myObject.b; // undefined
3.3.8 [[Put]]
  • 误解:可能会认为给对象的属性赋值会触发[[Put]]来设置或者创建这个属性。实际情况并不完全如此。
  • [[Put]]被触发时,实际行为取决于很多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。
  • 对象默认的[[Put]]操作可以控制属性值的设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
st=>start: Start
e=>end: End
cond1=>condition: 属性是否存在
op11=>operation: 请看第5章[[Prototype]]
cond2=>condition: 是否是访问描述符
(参见3.3.9)
op1=>operation: 存在setter就调用setter
cond3=>condition: 数据描述符中
writable为false?
op2=>operation: 非严格模式下静默失败,
严格模式下抛出TypeError异常
op3=>operation: 将该值设置为属性的值

st->cond1
cond1(yes)->cond2
cond1(no)->op11
cond2(yes)->op1->e
cond2(no)->cond3
cond3(yes)->op2->e
cond3(no)->op3->e
3.3.9 Getter和Setter
  • ES5中可以使用getter和setter部分改写[[Get]]和[[Put]]的默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter和setter是隐藏函数。
  • 当给属性定义getter、setter或者两者都有时,这个属性会被定义为”访问描述符“(和”数据描述符“相对)。会忽略它们的value和writable特性。关心的是set和get(还有configurable和enumerable)特性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 对象文字语法
var myObject = {
// 给a定义一个getter
get a() {
return 2;
}
};


// defineProperty(..)显式定义
Object.defineProperty(
myObject, // 目标对象
"b", // 属性名
{
// 给b设置一个getter访问描述符
get: function() { return this.a * 2 },

// 确保b会出现在对象的属性列表中
enumerable: true
}
);

myObject.a; // 2

myObject.b; // 4

myObject.a = 3; // 没有定义setter,set操作无效

myObject.a; // 2
  • 定义setter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var myObject = {
// 给a定义一个getter
get a() {
return this._a_; // _a_ 只是一种惯例,没有任何特殊的行为,和其他普通属性一样
},

// 给a定义一个setter
set a(val) {
this._a_ = val * 2;
}
};

myObject.a = 2;

myObject.a; // 4
3.3.10 存在性
  • in:检查属性名是否在对象及其[[Prototype]]原型链中

    • 对数组来说,4 in [2, 4, 6]返回false,因为这个数组包含的属性名是0、1、2
  • hasOwnProperty(..):只会检查属性名是否在对象中

    • 对于Object.create(null)对象没有连接到Object.prototype,此时需要Object.prototype.hasOwnProperty.call(myObject, "a")显式绑定到myObject。
1
2
3
4
5
6
7
8
9
var myObject = {
a: 2
};

("a" in myObject); // true
("b" in myObject); // false

myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
  • for..in循环:遍历对象的可枚举属性名列表,包括[[Prototype]]原型链。

    • 对于数组,不仅会包含所有数值索引,还会包含所有可枚举属性。遍历数组使用传统for循环
  • propertyIsEnumerable(..):只检查属性名是否在对象中并且enumerable:true

  • Object.keys(..):只查找属性名是否在对象中,返回一个数组,包含所有可枚举属性名

  • Object.getOwnPropertyNames(..):只查找属性名是否在对象中,返回一个数组,包含所有属性名,无论是否可枚举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var myObject = {};

Object.defineProperty(
myObject,
"a",
// 让a像普通属性一样可以枚举
{ enumerable: true, value: 2 }
);

Object.defineProperty(
myObject,
"b",
// 让b不可枚举
{ enumerable: false, value: 3}
);

myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty( "b" ); // true

for(var k in myObject) {
console.log( k, myObject[k] );
}
// "a" 2

myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false

Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]

3.4 遍历

遍历对象属性时的顺序是不确定的。

  • forEach(..)遍历数组中的所有值并忽略回调函数的返回值。

  • every(..)一直运行直到回调函数返回false(或者“假”值),会提前终止遍历

  • some(..)一直运行直到回调函数返回true(或者“真”值),会提前终止遍历

  • for..of:直接遍历值

    • 数组:直接遍历值而不是数组下标。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      var myArray = [ 1, 2, 3 ];

      for(var v of myArray) {
      console.log( v );
      }

      // 1
      // 2
      // 3
数组有内置的`@@iterator`,因此`for..of`可以直接应用在数组上。使用ES6的`Symbol.iterator`来获取对象的`@@iterator`内部属性。`@@iterator`本身并不是一个迭代器对象,而是一个**返回迭代器对象的函数**。

1
2
3
4
5
6
7
8
// 手动遍历数组
var myArray[ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();

it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { done: true }
  • 对象:定义了迭代器就可以遍历。先向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法遍历所有值。

    普通对象没有内置的@@iterator,无法自动完成for..of遍历。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // 手动给对象定义@@iterator

    var myObject = {
    a: 2,
    b: 3
    };

    Object.defineProperty( myObject, Symbol.iterator, {
    enumerable: false,
    writable: false,
    configurable: true,
    value: function() {
    var o = this;
    var idx = 0;
    var ks = Object.keys( o );
    return {
    next: function() {
    return {
    value: o[ks[idx++]],
    done: (idx > ks.length) // 结束条件
    }
    }
    }
    }
    } );

    // 手动遍历myObject
    var it = myArray[Symbol.iterator]();
    it.next(); // { value: 2, done: false }
    it.next(); // { value: 3, done: false }
    it.next(); // { value: undefined, done: true }

    // 用for..of遍历myObject
    for (var v of myObject ) {
    console.log( v );
    }
    // 2
    // 3

第4章 混合对象“类”

  • 类是一种设计模式。许多语言提供了对于面向类软件设计的原生语法。JS也有类似的语法,但是和其他语言中的类完全不同。
  • 类意味着复制。
  • 传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。
  • 多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父类,但是本质上引用的是复制的结果。
  • 多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制
  • JS并不会(像类那样)自动创建对象的副本,不会自动执行复制行为。
  • 混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆弱的语法,比如显式伪多态(OtherObj.methodName.call(this, ...)),会让代码更加难懂并且难以维护。
  • 显式混入实际上无法完全模拟类的复制行为,因为对象(函数也是对象)只能复制引用,无法复制被引用的对象或者函数本身。
  • 在JS中模拟类是得不偿失的。